/**
 * Escalonamento
 * --------------------------------------
 *
 * Quando eh necessario escalonar:
 *      - Quando cria um novo processo 
 *      - Quando termina um processo (removido) 
 *      - Quando ocorre uma interrupcao 
 *      - Quando eh bloqueado por I/O, semafaro ou outra situacao. 
 *
 * @author Caio Thomas Oliveira 11011BSI209
 * @author Leonardo Pimentel Ferreira	11011BSI223
 */


//#include "Escalonamento.h"       
#include "Constantes_Globals.h"

/**
 * Funcao que cria um rand de 1 ateh 50
 * @return inteiro
 */
int esc_get_quantum()
{
    return 1 + rand()%50;
}
/**
 * Funcao que escolhe qual agoritmo de escolamento ira executar
 * @return void
 */
void esc_escolhe_algoritmo_escalonamento()
{    
    int n = rand()%2;

    switch(n)
    {
        case 0:
            escalonarLoteria();
            break;
        case 1:
            escalonarCircular();
            break;
    }
}

void escalonarLoteria()
{               
    if(execucao == NULL || ed_processos_prontos->tamanho == 0)
        return;        
        
    fprintf(log_file,"----------------------------ESCALONAMENTO POR LOTERIA------------------------\n");
    fprintf(log_file,"\nFILA DE PROCESSOS PRONTO ANTES DO ESCALONAMENTO POR LOTERIA:");      
    ed_imprime(ed_processos_prontos);
  
    int menor_diferenca_bilhete_sorteado = 0; //é para controle de qual processo será sorteado, ou seja, o bilhete que mais se aproximar do bilhete sorteado
    int bilhete_sorteado = esc_get_quantum();    //é definido o número do bilhete premiado
           
    Processo* processo_sorteado;              //processo sorteado (o que vai ficar na 1ª posição da fila de prontos)         
    Lista* no = ed_processos_prontos->ini;    //para cada processo será dado um bilhete
    Lista* escolhido = no;                    //para passar como argumento da função: ed_altera_posicao_first(ed_processos_prontos, escolhido);
    no->processo->tabelaProcesso->quantum =  esc_get_quantum();       //processo da 1ª posição de pronto ganha de 1 a 50 bilhetes
    fprintf(log_file,"$$ O PROCESSO %d RECEBEU %d BILHETES\n",no->processo->pid, no->processo->tabelaProcesso->quantum);
    menor_diferenca_bilhete_sorteado = abs(no->processo->tabelaProcesso->quantum-bilhete_sorteado);     //abs é o módulo | o processo que tiver a menor diferença entre o blhete sorteado e ele será escolhido para executar
    processo_sorteado = no->processo;                                                          //processo sorteado inicialmente é o que está na primeira posição da fila de prontos
    no = no->prox;      //percorrer a fila de prontos e atribuir bilhetes aos processos
    
    while(no != NULL)
    {
        no->processo->tabelaProcesso->quantum = esc_get_quantum();
        fprintf(log_file,"$$ O PROCESSO %d RECEBEU %d BILHETES\n",no->processo->pid, no->processo->tabelaProcesso->quantum);
        if( abs(no->processo->tabelaProcesso->quantum-bilhete_sorteado) < menor_diferenca_bilhete_sorteado )
        {
            menor_diferenca_bilhete_sorteado = abs(no->processo->tabelaProcesso->quantum-bilhete_sorteado);
            processo_sorteado = no->processo;
            escolhido = no;
        }                
        no = no->prox;
    }
    
    fprintf(log_file,"\nO BILHETE SORTEADO FOI %d\n", bilhete_sorteado);
    fprintf(log_file,"O PROCESSO ESCOLHIDO POR LOTERIA FOI O %d COM FRAÇÃO DE QUANTUM DE CPU DE %d\n", processo_sorteado->pid, processo_sorteado->tabelaProcesso->quantum);
    ed_altera_posicao_first(ed_processos_prontos, escolhido);
    fprintf(log_file,"\nFILA DE PRONTOS APÓS ESCALONAMENTO POR LOTERIA\n");
    ed_imprime(ed_processos_prontos);        
    fprintf(log_file,"\n-----------------------------------------------------------------------------\n");
    return;
}


/**
 * Funcao escalonamento circular.
 * Eh atribuido um intervalo de tempo para executar.
 * Entrou como bloqueado (insere lista bloqueados) ou acabou tempo (insere processos prontos).
 */
void escalonarCircular()
{
    if(execucao == NULL || ed_processos_prontos->tamanho == 0)
        return;        
    
    fprintf(log_file,"----------------------------ESCALONAMENTO ROUND ROBIN------------------------\n");
    fprintf(log_file,"\nFILA DE PROCESSOS PRONTO ANTES DO ESCALONAMENTO POR CIRCULAR:");      
    ed_imprime(ed_processos_prontos);   
    
    Lista* no = ed_processos_prontos->ini;        
        
    while(no != NULL)
    {
        no->processo->tabelaProcesso->quantum = QUANTUM;//dando um quantum igual para todos da lista de processo prontos
        no = no->prox;
    }    
        
    fprintf(log_file,"O PROCESSO ESCOLHIDO POR ROUND ROBIN FOI O %d COM FRAÇÃO DE QUANTUM DE CPU DE %d\n", ed_processos_prontos->ini->processo->pid, ed_processos_prontos->ini->processo->tabelaProcesso->quantum);    
    fprintf(log_file,"\nFILA DE PRONTOS APÓS ESCALONAMENTO POR ROUND ROBIN\n");
    ed_imprime(ed_processos_prontos);        
    fprintf(log_file,"\n-----------------------------------------------------------------------------\n");     
}

/**
 * Funcao que insere o execucao para pronto e coloca um novo processo para execucao
 * @return void
 */
void esc_escalona()
{
    
    esc_exe_to_pronto();//execucao passa para pronto           
    
    esc_escolhe_algoritmo_escalonamento();
    
    esc_get_primeiro_fila_pronto_to_exe();//remove o primeiro da fila de pronto e coloca como execucao    
}

/**
 * Funcao que imprime quando um processo foi finalizado ou ocorreu erro fatal.
 * - altera na tabela de processo o estado para FIM_OU_ERRO_FATAL
 * @return void
 */
void esc_processo_finalizado()
{
    int i;
    if(execucao->errofatal == 0)
    {
            fprintf(log_file,"Processo PID %d Finalizado! - ", execucao->pid);
            fprintf(log_file,"RESULTADO: PID %d - N.Instrucoes %d - N.I/O %d - Quantum %d\n", execucao->pid, execucao->instrucoes, execucao->entradasaida, execucao->tabelaProcesso->quantum);                                 
            
            //vai liberar todos recursos                        
            recurso_liberar_recurso(execucao);
            
            //da null na tabela de processos do sistema
            for(i=0; i<TAM_MAX_PROCESSOS; i++)
                if(tabela_processos[i] != NULL)
                        if(tabela_processos[i]->id == execucao->tabelaProcesso->id)
                        {
                            fprintf(log_file, "Liberando tabela de processo pid %d.\n", tabela_processos[i]->id);
                            tabela_processos[i] = NULL;
                        }
                                
            
            verifica_processo_encerrou_estava_regiao_critica();
    }
    else{
            fprintf(log_file,"Xiii! Processo PID %d ocorreu um erro fatal! - Acho que foi divisao por zero.\n", execucao->pid);                        
            verifica_processo_encerrou_estava_regiao_critica();
    }
    
    execucao->tabelaProcesso->estado = FIM_OU_ERRO_FATAL;
}

/**
 * Funcao que remove o primeiro da fila de pronto e altera o estado para execucao
 * @return void
 */
void esc_get_primeiro_fila_pronto_to_exe()
{
    execucao = ed_remove_inicio(ed_processos_prontos,execucao);//remove o primeiro da fila de prontos
    salvaEstadoProcessoExecucao(EXECUTANDO);
    fprintf(log_file,"PROCESSO EM EXECUCAO PID: %d - recuperado da lista de pronto.\n", execucao->tabelaProcesso->id);            
}

/**
 * Funcao que insere o execucao na lista de pronto e altera o estado
 * @return void
 */
void esc_exe_to_pronto()
{
    fprintf(log_file,"ESCALONA: PID %d - N.Instrucoes %d - N.I/O %d - Quantum %d - inserindo na fila de pronto.\n", execucao->tabelaProcesso->id, execucao->instrucoes, execucao->entradasaida, execucao->tabelaProcesso->quantum);    

    ed_insere_fim(ed_processos_prontos, execucao);//insere processo em execucao na fila de pronto
    salvaEstadoProcessoExecucao(PRONTO);//salva o estado          
}

/**
 * Funcao que consiste em percorrer a lista de bloqueado e diminuindo a entrada e saida
 * quando for zero, o processo vai para fila de pronto
 * @return void
 */
void esc_verifica_bloqueio()
{
    Lista* q = ed_processos_bloqueados->ini;   
    while(q != NULL)
    {                  
        if(q->processo->entradasaida > 0){//decrementa i/o    
           while(rand()%2 == 0 && q->processo->entradasaida > 0){//processo faz IO
              q->processo->entradasaida--;//decrementa IO
           }
        }
        
        //se recurso nao estiver disponivel fica bloqueado, soh pode ser liberado quando
        //estiver disponivel e entrada e saida igual a zero e pode ir pra pronto
        if(q->processo->entradasaida == 0)
        {
            fprintf(log_file,"PROCESSO PRONTO PID %d - saindo da lista de bloqueado para pronto.\n", q->processo->pid);                        
            q->processo->tabelaProcesso->estado = PRONTO;//altera estado

            ed_insere_fim(ed_processos_prontos, q->processo);//insere processo em execucao na fila de pronto

            fprintf(log_file,"PID %d alterando estado para PRONTO.\n", q->processo->tabelaProcesso->id); 

            ed_remove_meio(ed_processos_bloqueados, q->processo);//retira processo da lista de bloqueados e passa para a fila de pronto                        
        }                
                    
        q = q->prox;  //vamos para o proximo processo bloqueado fazer IO
    } 
}

/**
 * Funcao responsavel por inserir um processo na lista de novos processos.
 * Com o decorrer da execucao dos processos vai retirando um  processo e colocando em execucao
 * junto com outos.
 * @param processo
 */
void esc_executa_processo(Processo* processo)
{               
    if(execucao != NULL && execucao->tabelaProcesso->estado == EXECUTANDO)
    {        
        salvaEstadoProcessoExecucao(PRONTO);
        ed_insere_fim(ed_processos_prontos, processo);
    }
    else
    {
        processo->tabelaProcesso->estado = EXECUTANDO;        
        execucao = processo;
    }    
    
    esc_escolhe_algoritmo_escalonamento();
    esc_executa();
}

/**
 * Funcao que fica responsavel de inserir novos processos de tempos em tempos 
 * durante a execucao.
 * - quando entra um novo processo na lista de prontos chamamos o escalonamento
 * @return void
 */
void esc_tempo_novo_processo()
{    
    if(ed_novos_processos->tamanho > 0 && tempo_para_novo_processo > 0)
            tempo_para_novo_processo--;
    else if(ed_novos_processos->tamanho > 0 && tempo_para_novo_processo == 0)
    {
        tempo_para_novo_processo = 1 + rand() % 5;//restaura o tempo que sera usado para inserir um novo processo

        /* - ocorre um escalonamento pois vai entrar um novo processo
            - remove o primeiro elemento da lista do novo processo e coloca em pronto
            - chama o escalona */           
        Processo *p;
        p = ed_remove_inicio(ed_novos_processos, p);
        ed_insere_fim(ed_processos_prontos, p);                               

        if(execucao->tabelaProcesso->estado == EXECUTANDO)//soh apresenta se o processo estiver em execucao
        {
            fim_processo(); //calcula o tempo final do processo
        }         
            
        fprintf(log_file,"+ chegando um novo processo PID %d.\n", p->pid);                                              
        
        //soh escalonamos quando temos um processo em execucao e uma lista de prontos
        //caso contrario o proximo processo ira executar
        if(ed_processos_prontos->tamanho > 0 && execucao->tabelaProcesso->estado == EXECUTANDO)
          esc_escalona();        
    }    
}

/**
 * Atencao: funcao recursiva
 * Funcao que faz as transicoes dos estados para o escalonamento.
 * 0 - PROCESSO FINALIZOU OU OCORREU UM ERRO FATAL E SAIU
 * 1 - PROCESSO PRONTO
 * 2 - PROCESSO BLOQUEADO
 * 3 - PROCESSO EM EXECUCAO
 * - A funcao sai quando nao temos processos bloqueado e nem pronto e nenhum executando.
 * @return recursivo
 */
void esc_executa()
{        
    inicio = 0;
    fim = 0;    
    //com o passar do tempo vamos inserir novos processos na lista de prontos
    //quando entra um novo processo na lista de prontos chamamos o escalonamento
    esc_tempo_novo_processo();    
    /**
     * Condicao de saida: ocorre quando o ultimo processo ja executou todas entradas e saidas,
     * seu estado ficou 0 (finalizado ou ocorreu um erro fatal), nao temos nenhum processo na fila de prontos e nenhum
     * na fila de bloqueados.
     */   
    if(ed_processos_prontos->tamanho    == 0 && 
       ed_processos_bloqueados->tamanho == 0 &&
       execucao->tabelaProcesso->estado == FIM_OU_ERRO_FATAL)
    {
        execucao = NULL;
        fprintf(log_file,"\nOcioso pelo Sistema.\nSaida do Sistema.\n");
        return;
    }
    
    /**
     * Quando o processo executacao esta bloqueado ou finalizado e nao temos nenhum na fila de processos prontos.
     * Entao o sistema ficara ocioso.
     */
    if(ed_processos_prontos->tamanho 	== 0 &&        
      (execucao->tabelaProcesso->estado == BLOQUEADO ||  execucao->tabelaProcesso->estado == FIM_OU_ERRO_FATAL))
    {
         fprintf(log_file,"Nenhum processo pronto! - PROCESSO EM EXECUCAO: Ocioso pelo Sistema.\n");
    }
        
    /**
     * Aqui executa um processo, sera executado instrucoes, verifica quando ocorre i/o e 
     * se tem algum Erro Fatal.
     */
    if(execucao->tabelaProcesso->quantum > 0 && execucao->tabelaProcesso->estado == EXECUTANDO)
    {
        inicio = 0;
        inicio = startTime();
        fprintf(log_file, "Executando: PID %d - N.Instrucoes %d - N.I/O %d - Quantum %d.\n", execucao->pid, execucao->instrucoes, execucao->entradasaida, execucao->tabelaProcesso->quantum);                
    }
            
    while(execucao->tabelaProcesso->quantum > 0 && execucao->tabelaProcesso->estado == EXECUTANDO)
    {                        
        esc_tempo_novo_processo();
        
        execucao->tabelaProcesso->quantum--;//decrementa o quantum ateh 0
        execucao->tabelaProcesso->contadorPrograma++;                         
        
        if(rand() % 2 == 0 && execucao->instrucoes > 0)
        {                           
            if(rand() % 2 == 1 && execucao->regiaocritica > 0)//simula regiao critica                
            {
                   int impedido = 1;
                   
 
                   if(execucao->pid != pid_processo_regiao_critica)
                   {                       
                       impedido = mutex_lock(execucao);                                                                                          
                   }
                   
                   if(impedido == 0 || (execucao->pid == pid_processo_regiao_critica))//pode entrar na regiao critica
                   {
                       execucao->regiaocritica=rand()%2;
                       
                       if(execucao->regiaocritica == 0)
                       { 
                          fprintf(log_file,"Processo %d saiu da regiao critica.\n",execucao->pid);
                          mutex_unlock(execucao);
                       }
                       else//escalona
                       {                                     
                          salvaEstadoProcessoExecucao(BLOQUEADO);//altera estado para bloqueado
                          
                          ed_insere_fim(ed_processos_bloqueados, execucao);            
                          
                          if(ed_processos_prontos->tamanho > 0)//recupera um novo processo
                          {
                               esc_escolhe_algoritmo_escalonamento();              
                               execucao = ed_remove_inicio(ed_processos_prontos,execucao);
                               fprintf(log_file,"PROCESSO EM EXECUCAO PID: %d - recuperado da lista de pronto.\n", execucao->tabelaProcesso->id);
                               execucao->tabelaProcesso->estado = EXECUTANDO;                    
                               return esc_executa();//recursividade para executar o processo novamente
                          }                                                               
                      }
                   }
                   else//escalona
                   {                                    
                       salvaEstadoProcessoExecucao(BLOQUEADO);//altera estado para bloqueado
                       ed_insere_fim(ed_processos_bloqueados, execucao);    
                       
                       if(ed_processos_prontos->tamanho > 0)//recupera um novo processo
                       {
                           esc_escolhe_algoritmo_escalonamento();              
                           execucao = ed_remove_inicio(ed_processos_prontos,execucao);                    
                           fprintf(log_file,"PROCESSO EM EXECUCAO PID: %d - recuperado da lista de pronto.\n", execucao->tabelaProcesso->id);                    
                           execucao->tabelaProcesso->estado = EXECUTANDO;                                        
                           return esc_executa();//recursividade para executar o processo novamente
                        }                                                                                   
                    }                
            }
            execucao->instrucoes--;//se for par decrementa instrucoes                           
        }
        else if ((execucao->instrucoes % 2 == 0) && (execucao->errofatal == 1) )
        {                        
            //ocorreu um erro fatal            
            //ocorre um erro fatal quando o processo->errofatal eh igual a 1
            //essas codicoes saem da execucao e vai para condicao de erro fatal
            execucao->instrucoes = 0;
            execucao->entradasaida = 0;
            execucao->tabelaProcesso->quantum = 0;
        }
        else if (execucao->entradasaida > 0)                
        {           
                    
            fim_processo(); //calcula o tempo final do processo
            fprintf(log_file,"PROCESSO BLOQUEADO PID: %d - inserindo na lista de bloqueado.\n",execucao->tabelaProcesso->id);     
            
            salvaEstadoProcessoExecucao(BLOQUEADO);//altera estado para bloqueado
            ed_insere_fim(ed_processos_bloqueados, execucao);
            
            if(ed_processos_prontos->tamanho > 0)//recupera um novo processo
            {
                    esc_escolhe_algoritmo_escalonamento();              
                    execucao = ed_remove_inicio(ed_processos_prontos,execucao);
                    
                    fprintf(log_file,"PROCESSO EM EXECUCAO PID: %d - recuperado da lista de pronto.\n", execucao->tabelaProcesso->id);
                    
                    execucao->tabelaProcesso->estado = EXECUTANDO;                    
                    
                    return esc_executa();//recursividade para executar o processo novamente
            }                               
        }
    }
    
    if(execucao->tabelaProcesso->estado == EXECUTANDO)
    {        
        fim_processo(); //calcula o tempo final do processo
    }         
    
    /**
     * Muda o estado se o processo finalizou.
     * Verifica se a execucao atual esta no estado 3 (execucao), observa se os numeros de 
     * instrucoes e i/o forem 0. Altera o estado do execucao para 0 (finalizado).
     * Quando um processo finaliza: ocorre quando o numero de instrucoes e i/o forem 0.
     * O estado eh 0 (finalizado)
     */
    if(execucao->instrucoes == 0        &&
       execucao->entradasaida == 0      &&
       execucao->tabelaProcesso->estado == EXECUTANDO)
    {
        esc_processo_finalizado();
    }
    
    esc_verifica_bloqueio(); //verifica se foi desbloqueado e insere na lista de prontos, vai dando -- no i/o ateh 0    

    /**
     * Quando vamos escalonar de Execucao para pronto.
     * Quando o processo atual acabou o quantum, possui instrucoes e i/o maiores que zero,
     * seu estado de execucao eh 3. Entao escalona processo de execucao para pronto.
     * Altera o estado para pronto (1) e da um quantum novo.
     * Insere processo em execucao na lista de prontos.
     * Recupera um novo processo se a fila de prontos tiver processos.
     */
    if(execucao->tabelaProcesso->estado == EXECUTANDO && ed_processos_prontos->tamanho > 0 &&
      (execucao->instrucoes > 0 || execucao->entradasaida > 0))
    {
        esc_exe_to_pronto();//execucao passa para pronto        
        esc_escolhe_algoritmo_escalonamento();
        esc_get_primeiro_fila_pronto_to_exe();//remove o primeiro da fila de pronto e coloca como execucao
        
        return esc_executa();//recursividade 
    }
    
    //recupera um novo processo se o tamanho for maior que 0
    if(ed_processos_prontos->tamanho > 0)
    {      
        esc_escolhe_algoritmo_escalonamento();
        esc_get_primeiro_fila_pronto_to_exe();//remove o primeiro da fila de pronto e coloca como execucao       
        
        return esc_executa();//recursividade 
    }     

    /**
     * Se nao tem processo pronto, e ainda tem processo em execucao, soh que o quantum dele acabou
     * ele recebe mais quantum para continuir a executar
     */
    if(ed_processos_prontos->tamanho == 0 &&        
       execucao->tabelaProcesso->estado == EXECUTANDO &&
      (execucao->instrucoes > 0 || execucao->entradasaida > 0))
    {
        execucao->tabelaProcesso->quantum = esc_get_quantum();
        fprintf(log_file,"Nenhum processo pronto, dando mais quantum para processo atual com fracao %d de CPU.\n",  execucao->tabelaProcesso->quantum);
    }                
    
    return esc_executa();//recursividade        
}

void salvaEstadoProcessoExecucao(int proximo_estado_do_processo_execucao)
{    
    
    switch(proximo_estado_do_processo_execucao)
    {
        case BLOQUEADO:
            fprintf(log_file, "PID %d alterando estado para BLOQUEADO.\n", execucao->tabelaProcesso->id);    
            break;
            
        case PRONTO:
            fprintf(log_file, "PID %d alterando estado para PRONTO.\n", execucao->tabelaProcesso->id);                
            break;
            
        case EXECUTANDO:
            fprintf(log_file, "PID %d alterando estado para EXECUCAO.\n", execucao->tabelaProcesso->id);            
            execucao->tabelaProcesso->estado = proximo_estado_do_processo_execucao;           
            return;
            break;
    }
    
    fprintf(log_file,"Alterando estado de modo usuário para modo núcleo...\n");
    fprintf(log_file,"Salvando dados processo %d na tabela de processo\n", execucao->pid);
    fprintf(log_file,"Salvando Contador de programa do processo ...\n");    
    fprintf(log_file,"Salvando Arquivos Abertos do processo ...\n");    
    
    //OBS: se tiver mapa de memória a mesma também deverá ser salva
    execucao->tabelaProcesso->estado = proximo_estado_do_processo_execucao;           
}

void fim_processo()
{
    fim = 0;
    fim = pauseTime();    
    fprintf(log_file,"O processo PID %d ficou %fms na CPU.\n",execucao->tabelaProcesso->id, currentTime(inicio,fim));    
    verifica_processo_encerrou_estava_regiao_critica();
}

void verifica_processo_encerrou_estava_regiao_critica()
{
    if(pid_processo_regiao_critica == execucao->pid)
    {
        fprintf(log_file,"Processo %d encerrou e estava na regiao critica.\n",execucao->pid);
        pid_processo_regiao_critica = 0;
        mutex_unlock();
    }
}